feat(ui): add CognitiveMeshPanel, SystemHealthBar, and cogmesh-poller#3
feat(ui): add CognitiveMeshPanel, SystemHealthBar, and cogmesh-poller#3JustAGhosT merged 2 commits intomainfrom
Conversation
- Add CognitiveMeshPanel: workflow cards with stage progress, health badge,
fallback indicator, and view-result links; hidden when unconfigured
- Add SystemHealthBar: service health pills in AgentFleetPanel header;
shows CM status (connected/degraded/unreachable/unconfigured), hidden
when all services are unconfigured
- Add cogmesh-poller: polls CM /health every 30 s, emits cogmesh:health:updated
via WebSocket broadcast; reloads on .retortconfig change
- Add parsers/retortconfig.ts: reads cogmesh.endpoint/secret from .retortconfig
with ${VAR} env var substitution
- Extend protocol.ts + bridge/types.ts: CognitiveMeshHealth discriminated union,
7 CM escalation fields on AgentTask, cogmesh:health:updated HostMessage
- Add 'cogmesh' ActivePanel + CogmeshDot status indicator in App.tsx
- Wire cogmeshHealth into useStore
Closes retort-plugins#2
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Mention Blocks like a regular teammate with your question or request: @blocks review this pull request Run |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Important Review skippedReview was skipped due to path filters ⛔ Files ignored due to path filters (1)
CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThis PR implements cognitive mesh health monitoring and a UI panel to surface workflow status. The state-watcher parses Changes
Sequence DiagramsequenceDiagram
participant UI as React UI
participant WS as WebSocket Bridge
participant SW as State-Watcher
participant FS as File System
participant CM as Cognitive Mesh API
SW->>FS: Read .retortconfig
FS-->>SW: endpoint + secret
loop Every 30 seconds
SW->>CM: GET /health (Bearer auth, 5s timeout)
alt Health OK
CM-->>SW: { ok: true, latencyMs }
SW->>SW: Set status: connected/degraded
else Request fails
CM-->>SW: Timeout/Network error
SW->>SW: Set status: unreachable
end
SW->>WS: Broadcast cogmesh:health:updated
WS->>UI: Message received
UI->>UI: Update cogmeshHealth state
UI->>UI: Render CogmeshDot + SystemHealthBar
end
Note over SW,UI: User triggers reload (e.g., config change)
SW->>SW: Re-parse .retortconfig
SW->>CM: Immediate health check
SW->>WS: Broadcast updated health
WS->>UI: Refresh health display
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (5)
packages/state-watcher/src/cogmesh-poller.ts (2)
46-49: Rapidreload()calls can cause overlapping health checks.If
.retortconfigchanges trigger multiple rapidreload()calls (before debouncing in index.ts), severalcheckHealth()invocations could run concurrently, potentially emitting out-of-order health updates.Consider tracking an in-flight request to skip or abort previous checks:
🔧 Option: guard against concurrent checks
+ let abortController: AbortController | null = null + async function checkHealth(): Promise<void> { + abortController?.abort() + abortController = new AbortController() + if (!config.endpoint) { onHealth({ status: 'unconfigured' }) return } const url = `${config.endpoint.replace(/\/$/, '')}/health` const headers: Record<string, string> = {} if (config.secret) headers['Authorization'] = `Bearer ${config.secret}` const start = Date.now() try { const res = await fetch(url, { headers, - signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS), + signal: AbortSignal.any([ + abortController.signal, + AbortSignal.timeout(HEALTH_TIMEOUT_MS), + ]), })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/state-watcher/src/cogmesh-poller.ts` around lines 46 - 49, The reload() function calls checkHealth() unconditionally which allows overlapping health checks when reload() is invoked rapidly; modify reload() and checkHealth() to track an in-flight check (e.g., an inFlight boolean or an AbortController stored at module scope) so that reload() either cancels the previous check before starting a new one or skips starting a new check while one is running; update references to config populated by parseCogmeshConfig(root) accordingly so the running check can read the correct config snapshot and ensure checkHealth() respects the cancellation/guard to prevent concurrent executions and out-of-order health updates.
51-54:stop()doesn't cancel in-flight requests.Calling
stop()clears the interval but any ongoingcheckHealth()fetch will continue to completion and callonHealth()after the poller is "stopped". This is likely benign but worth noting if shutdown ordering matters.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/state-watcher/src/cogmesh-poller.ts` around lines 51 - 54, The stop() function currently only clears the interval (timer) but does not cancel any in-flight checkHealth() fetches, so add cancellation: introduce an AbortController (or an inFlightController) used by checkHealth() when calling fetch/HTTP client and store it on the poller; update stop() to call controller.abort() and set timer = null and a stopped flag; update checkHealth() to accept/use the AbortSignal and to skip calling onHealth() if the request was aborted (catch AbortError or check the stopped flag) so no callbacks run after stop() is called. Ensure you reference and modify stop(), checkHealth(), and onHealth() accordingly.packages/state-watcher/src/parsers/retortconfig.ts (2)
33-36: Static analysis flags ReDoS risk — low practical concern given controlled keys.The
keyparameter is always hardcoded ('endpoint','secret') from withinparseCogmeshConfig, so the regex pattern is effectively static. However, if this helper were reused with untrusted keys in the future, the dynamic regex construction could become a vulnerability.Consider extracting dedicated functions or using a key whitelist if the parser expands.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/state-watcher/src/parsers/retortconfig.ts` around lines 33 - 36, The extractScalar function builds a dynamic RegExp from the provided key which poses a ReDoS risk if untrusted keys are ever passed; restrict usage by either only accepting the known keys used by parseCogmeshConfig ('endpoint' and 'secret') or replace the dynamic-regex approach with a safe lookup: precompute/compile separate regexes for those known keys (or whitelist and escape the key before building the RegExp) and update extractScalar (or introduce extractEndpoint/extractSecret) accordingly so no arbitrary/unescaped key is interpolated into a RegExp at runtime.
38-41: Unresolved env vars are preserved — verify this is intended.When
${VAR}references an undefined environment variable, the placeholder is kept verbatim (e.g., the endpoint becomes"https://${MISSING}/api"). This could cause confusing runtime errors from the poller trying to fetch a malformed URL.Consider logging a warning or returning
nullwhen env substitution fails.🔧 Option: warn on unresolved vars
function resolveEnv(value: string | null): string | null { if (!value) return null - return value.replace(/\$\{([^}]+)\}/g, (_, name: string) => process.env[name] ?? `\${${name}}`) + return value.replace(/\$\{([^}]+)\}/g, (match, name: string) => { + const resolved = process.env[name] + if (resolved === undefined) { + process.stderr.write(`[retortconfig] Warning: env var ${name} is not set\n`) + } + return resolved ?? match + }) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/state-watcher/src/parsers/retortconfig.ts` around lines 38 - 41, The resolveEnv function currently preserves unresolved ${VAR} placeholders which can produce malformed URLs at runtime; update resolveEnv to detect any missing env names during replacement (e.g., collect names where process.env[name] is undefined) and if any are missing either log a warning (including the original input and the missing variable names) and return null, or at minimum return null when any placeholder remains unresolved; implement this in resolveEnv so callers (like the poller) get a null instead of a broken string and can handle the error upstream.packages/state-watcher/src/parsers/teams.ts (1)
21-29: Potential issue with untrusted marker file content.If
.agentkit-repocontains path separators, leading/trailing slashes, or special characters, the resulting overlay path could resolve unexpectedly (e.g., path traversal or broken paths). Consider sanitizing the marker content.🛡️ Proposed sanitization
function detectRepoName(root: string): string { try { const markerPath = path.join(root, '.agentkit-repo') if (fs.existsSync(markerPath)) { - return fs.readFileSync(markerPath, 'utf-8').trim() + const raw = fs.readFileSync(markerPath, 'utf-8').trim() + // Sanitize: strip path separators and use only the basename portion + const sanitized = path.basename(raw.replace(/[\\/]/g, '-')) + return sanitized || path.basename(root) } } catch { /* ignore */ } return path.basename(root) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/state-watcher/src/parsers/teams.ts` around lines 21 - 29, The detectRepoName function reads untrusted .agentkit-repo content and returns it raw which can produce unsafe or unexpected paths; update detectRepoName to sanitize the marker content read from markerPath: trim it, reject or strip path separators and any path traversal tokens (e.g., ".."), and restrict characters (e.g., allow only alphanumerics, dots, underscores, hyphens) or otherwise take path.basename() of the content; if the sanitized result is empty or invalid, fall back to path.basename(root) to avoid creating unsafe overlay paths. Ensure these checks are applied to the value returned by fs.readFileSync before returning from detectRepoName.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/state-watcher/src/cogmesh-poller.ts`:
- Around line 31-35: The project uses AbortSignal.timeout and global fetch (seen
in cogmesh-poller.ts with AbortSignal.timeout(HEALTH_TIMEOUT_MS) and fetch), so
add an explicit engines.node field to both the root package.json and
packages/state-watcher/package.json to require Node.js >=18.0.0 (or a higher
minimum you prefer); update each package.json's "engines" section to include
"node": ">=18.0.0" so CI and consumers are warned/enforced about the Node
version compatibility.
In `@packages/ui/src/panels/CognitiveMeshPanel.tsx`:
- Around line 143-149: The formatAge function can produce "NaNh ago" for
malformed ISO strings; update formatAge to validate the parsed timestamp (e.g.,
const ts = new Date(iso).getTime()) and check Number.isFinite(ts) or !isNaN(ts)
before computing diffMs; if the timestamp is invalid, return a safe fallback
like 'unknown' or 'just now' to avoid leaking bad state into the UI. Ensure all
existing branches (mins < 1, mins < 60, hours) remain unchanged once the parsed
timestamp is validated.
- Around line 20-25: In StageBar, the calculated pct can go out of 0–100 when
upstream provides unexpected stage/total values; clamp the computed percent to
the 0–100 range (e.g., compute raw percent when total>0 then apply Math.max(0,
Math.min(100, rawPct))) so the inline style width never becomes invalid and
handle total<=0 by using 0.
In `@packages/ui/src/styles/index.css`:
- Line 330: The CSS declaration "border: 1px solid currentColor" uses camelCase
and violates the Stylelint value-keyword-case rule; update the declaration by
replacing the token currentColor with lowercase currentcolor (i.e., change the
value in the "border: 1px solid currentColor" rule), then re-run stylelint to
confirm the violation is resolved.
---
Nitpick comments:
In `@packages/state-watcher/src/cogmesh-poller.ts`:
- Around line 46-49: The reload() function calls checkHealth() unconditionally
which allows overlapping health checks when reload() is invoked rapidly; modify
reload() and checkHealth() to track an in-flight check (e.g., an inFlight
boolean or an AbortController stored at module scope) so that reload() either
cancels the previous check before starting a new one or skips starting a new
check while one is running; update references to config populated by
parseCogmeshConfig(root) accordingly so the running check can read the correct
config snapshot and ensure checkHealth() respects the cancellation/guard to
prevent concurrent executions and out-of-order health updates.
- Around line 51-54: The stop() function currently only clears the interval
(timer) but does not cancel any in-flight checkHealth() fetches, so add
cancellation: introduce an AbortController (or an inFlightController) used by
checkHealth() when calling fetch/HTTP client and store it on the poller; update
stop() to call controller.abort() and set timer = null and a stopped flag;
update checkHealth() to accept/use the AbortSignal and to skip calling
onHealth() if the request was aborted (catch AbortError or check the stopped
flag) so no callbacks run after stop() is called. Ensure you reference and
modify stop(), checkHealth(), and onHealth() accordingly.
In `@packages/state-watcher/src/parsers/retortconfig.ts`:
- Around line 33-36: The extractScalar function builds a dynamic RegExp from the
provided key which poses a ReDoS risk if untrusted keys are ever passed;
restrict usage by either only accepting the known keys used by
parseCogmeshConfig ('endpoint' and 'secret') or replace the dynamic-regex
approach with a safe lookup: precompute/compile separate regexes for those known
keys (or whitelist and escape the key before building the RegExp) and update
extractScalar (or introduce extractEndpoint/extractSecret) accordingly so no
arbitrary/unescaped key is interpolated into a RegExp at runtime.
- Around line 38-41: The resolveEnv function currently preserves unresolved
${VAR} placeholders which can produce malformed URLs at runtime; update
resolveEnv to detect any missing env names during replacement (e.g., collect
names where process.env[name] is undefined) and if any are missing either log a
warning (including the original input and the missing variable names) and return
null, or at minimum return null when any placeholder remains unresolved;
implement this in resolveEnv so callers (like the poller) get a null instead of
a broken string and can handle the error upstream.
In `@packages/state-watcher/src/parsers/teams.ts`:
- Around line 21-29: The detectRepoName function reads untrusted .agentkit-repo
content and returns it raw which can produce unsafe or unexpected paths; update
detectRepoName to sanitize the marker content read from markerPath: trim it,
reject or strip path separators and any path traversal tokens (e.g., ".."), and
restrict characters (e.g., allow only alphanumerics, dots, underscores, hyphens)
or otherwise take path.basename() of the content; if the sanitized result is
empty or invalid, fall back to path.basename(root) to avoid creating unsafe
overlay paths. Ensure these checks are applied to the value returned by
fs.readFileSync before returning from detectRepoName.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: b8019986-a916-4ad4-93e7-8ee0634107f4
📒 Files selected for processing (12)
packages/state-watcher/src/cogmesh-poller.tspackages/state-watcher/src/index.tspackages/state-watcher/src/parsers/retortconfig.tspackages/state-watcher/src/parsers/teams.tspackages/state-watcher/src/protocol.tspackages/ui/src/App.tsxpackages/ui/src/bridge/types.tspackages/ui/src/bridge/useStore.tspackages/ui/src/components/SystemHealthBar.tsxpackages/ui/src/panels/AgentFleetPanel.tsxpackages/ui/src/panels/CognitiveMeshPanel.tsxpackages/ui/src/styles/index.css
| try { | ||
| const res = await fetch(url, { | ||
| headers, | ||
| signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS), | ||
| }) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check for Node.js version requirements in package.json or similar configs
fd -t f 'package.json' --exec cat {} \; | jq -r '.engines.node // empty' 2>/dev/null || echo "No engines.node found"Repository: phoenixvc/retort-plugins
Length of output: 50
🏁 Script executed:
# Check if package.json exists and examine its full contents
cd packages/state-watcher && cat package.json || echo "No package.json found in state-watcher"Repository: phoenixvc/retort-plugins
Length of output: 631
🏁 Script executed:
# Also check project root package.json
cat package.json 2>/dev/null || echo "No package.json in root"Repository: phoenixvc/retort-plugins
Length of output: 885
🏁 Script executed:
# Check for any Node.js version constraint files
fd -t f '\.nvmrc|\.node-version|\.tool-versions' -x cat {}Repository: phoenixvc/retort-plugins
Length of output: 50
Add explicit engines.node field to package.json to clarify minimum Node.js version.
The code uses AbortSignal.timeout() (requires Node.js ≥17.3) and global fetch (requires Node.js ≥18). While the project's @types/node dependency is pinned to ^20.0.0, suggesting Node.js 20+, there is no explicit engines.node field in either packages/state-watcher/package.json or the root package.json. Add "engines": { "node": ">=18.0.0" } (or higher) to both package.json files to enforce this requirement and prevent accidental use with incompatible Node versions.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/state-watcher/src/cogmesh-poller.ts` around lines 31 - 35, The
project uses AbortSignal.timeout and global fetch (seen in cogmesh-poller.ts
with AbortSignal.timeout(HEALTH_TIMEOUT_MS) and fetch), so add an explicit
engines.node field to both the root package.json and
packages/state-watcher/package.json to require Node.js >=18.0.0 (or a higher
minimum you prefer); update each package.json's "engines" section to include
"node": ">=18.0.0" so CI and consumers are warned/enforced about the Node
version compatibility.
| function StageBar({ stage, total }: { stage: number; total: number }) { | ||
| const pct = total > 0 ? Math.round((stage / total) * 100) : 0 | ||
| return ( | ||
| <div className="cm-stage-bar" title={`Stage ${stage} of ${total}`}> | ||
| <div className="cm-stage-fill" style={{ width: `${pct}%` }} /> | ||
| </div> |
There was a problem hiding this comment.
Clamp progress percent to avoid invalid/out-of-range widths.
If upstream sends unexpected values (e.g., stage > total), Line 24 can produce widths above 100%.
🔧 Proposed fix
function StageBar({ stage, total }: { stage: number; total: number }) {
- const pct = total > 0 ? Math.round((stage / total) * 100) : 0
+ const raw = total > 0 ? (stage / total) * 100 : 0
+ const pct = Math.max(0, Math.min(100, Math.round(raw)))
return (
<div className="cm-stage-bar" title={`Stage ${stage} of ${total}`}>
<div className="cm-stage-fill" style={{ width: `${pct}%` }} />
</div>
)
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function StageBar({ stage, total }: { stage: number; total: number }) { | |
| const pct = total > 0 ? Math.round((stage / total) * 100) : 0 | |
| return ( | |
| <div className="cm-stage-bar" title={`Stage ${stage} of ${total}`}> | |
| <div className="cm-stage-fill" style={{ width: `${pct}%` }} /> | |
| </div> | |
| function StageBar({ stage, total }: { stage: number; total: number }) { | |
| const raw = total > 0 ? (stage / total) * 100 : 0 | |
| const pct = Math.max(0, Math.min(100, Math.round(raw))) | |
| return ( | |
| <div className="cm-stage-bar" title={`Stage ${stage} of ${total}`}> | |
| <div className="cm-stage-fill" style={{ width: `${pct}%` }} /> | |
| </div> | |
| ) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/ui/src/panels/CognitiveMeshPanel.tsx` around lines 20 - 25, In
StageBar, the calculated pct can go out of 0–100 when upstream provides
unexpected stage/total values; clamp the computed percent to the 0–100 range
(e.g., compute raw percent when total>0 then apply Math.max(0, Math.min(100,
rawPct))) so the inline style width never becomes invalid and handle total<=0 by
using 0.
| function formatAge(iso: string): string { | ||
| const diffMs = Date.now() - new Date(iso).getTime() | ||
| const mins = Math.floor(diffMs / 60_000) | ||
| if (mins < 1) return 'just now' | ||
| if (mins < 60) return `${mins}m ago` | ||
| return `${Math.floor(mins / 60)}h ago` | ||
| } |
There was a problem hiding this comment.
Guard invalid timestamps in formatAge.
If iso is malformed, Line 148 can render NaNh ago, which leaks bad state into UI text.
🔧 Proposed fix
function formatAge(iso: string): string {
- const diffMs = Date.now() - new Date(iso).getTime()
+ const ts = new Date(iso).getTime()
+ if (!Number.isFinite(ts)) return 'unknown'
+ const diffMs = Math.max(0, Date.now() - ts)
const mins = Math.floor(diffMs / 60_000)
if (mins < 1) return 'just now'
if (mins < 60) return `${mins}m ago`
return `${Math.floor(mins / 60)}h ago`
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function formatAge(iso: string): string { | |
| const diffMs = Date.now() - new Date(iso).getTime() | |
| const mins = Math.floor(diffMs / 60_000) | |
| if (mins < 1) return 'just now' | |
| if (mins < 60) return `${mins}m ago` | |
| return `${Math.floor(mins / 60)}h ago` | |
| } | |
| function formatAge(iso: string): string { | |
| const ts = new Date(iso).getTime() | |
| if (!Number.isFinite(ts)) return 'unknown' | |
| const diffMs = Math.max(0, Date.now() - ts) | |
| const mins = Math.floor(diffMs / 60_000) | |
| if (mins < 1) return 'just now' | |
| if (mins < 60) return `${mins}m ago` | |
| return `${Math.floor(mins / 60)}h ago` | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/ui/src/panels/CognitiveMeshPanel.tsx` around lines 143 - 149, The
formatAge function can produce "NaNh ago" for malformed ISO strings; update
formatAge to validate the parsed timestamp (e.g., const ts = new
Date(iso).getTime()) and check Number.isFinite(ts) or !isNaN(ts) before
computing diffMs; if the timestamp is invalid, return a safe fallback like
'unknown' or 'just now' to avoid leaking bad state into the UI. Ensure all
existing branches (mins < 1, mins < 60, hours) remain unchanged once the parsed
timestamp is validated.
| font-size: 11px; | ||
| padding: 1px 7px; | ||
| border-radius: 10px; | ||
| border: 1px solid currentColor; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify occurrences of currentColor/currentcolor in the updated stylesheet
rg -n 'currentColor|currentcolor' packages/ui/src/styles/index.cssRepository: phoenixvc/retort-plugins
Length of output: 105
🏁 Script executed:
#!/bin/bash
# Search for stylelint configuration files
find . -maxdepth 3 -type f \( -name '.stylelintrc*' -o -name '.stylelintignore' \) 2>/dev/null | head -20
# Also check package.json for stylelint config
if [ -f package.json ]; then
cat package.json | grep -A 10 '"stylelint"'
fiRepository: phoenixvc/retort-plugins
Length of output: 87
🌐 Web query:
CSS currentColor keyword case sensitivity stylelint value-keyword-case rule
💡 Result:
The CSS currentColor keyword is case-insensitive in all browsers. CSS keywords are ASCII case-insensitive per CSS2/CSS3 specs, so currentColor, currentcolor, and CURRENTCOLOR all work identically, resolving to the element's color property value. This is confirmed across Chrome, Firefox, Safari, and Edge devtools, which accept any casing despite varying autocomplete preferences. The stylelint value-keyword-case rule, when set to "lower" (default in many configs), flags camelCase currentColor as invalid and suggests currentcolor, aligning with CSS Color Level 4 spec notation (lowercase currentcolor). However, it previously treated SVG-originated keywords like currentColor as exceptions requiring camelCase. To allow camelCase currentColor with "lower" option, enable the camelCaseSvgKeywords: true secondary option: { "rules": { "value-keyword-case": ["lower", { "camelCaseSvgKeywords": true }] } } This permits camelCase for SVG keywords (e.g., currentColor, optimizeQuality) while enforcing lowercase for standard keywords (e.g., block, flex). Without it, use "currentcolor" or ignoreKeywords: ["currentColor"] / ignoreProperties: ["color"] as workarounds. camelCaseSvgKeywords defaults to false, prioritizing spec's lowercase preference. All browser support remains unchanged regardless of casing used.
Citations:
- 1: value-keyword-case for currentColor in non SVG CSS is breaking code stylelint/stylelint#5863
- 2: https://stackoverflow.com/questions/17967371/are-property-values-in-css-case-sensitive
- 3: https://stylelint.io/user-guide/rules/value-keyword-case/
- 4: Add camelCaseSvgKeywords to value-keyword-case and set default to false stylelint/stylelint#4884
- 5: https://w3.org/TR/css-color-4
- 6: https://drafts.csswg.org/css-color/
- 7: https://www.w3.org/TR/2026/CRD-css-color-4-20260326/
- 8: https://stylelint.io/user-guide/rules/value-keyword-case
🏁 Script executed:
#!/bin/bash
cat .stylelintrc.jsonRepository: phoenixvc/retort-plugins
Length of output: 1074
Change currentColor to lowercase currentcolor to comply with Stylelint's default value-keyword-case rule.
The repository's Stylelint configuration extends stylelint-config-standard-scss without overriding the value-keyword-case rule, which defaults to enforcing lowercase keywords. Line 330 contains currentColor in camelCase, which triggers this violation.
🔧 Proposed fix
font-size: 11px;
padding: 1px 7px;
border-radius: 10px;
- border: 1px solid currentColor;
+ border: 1px solid currentcolor;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| border: 1px solid currentColor; | |
| font-size: 11px; | |
| padding: 1px 7px; | |
| border-radius: 10px; | |
| border: 1px solid currentcolor; | |
| } |
🧰 Tools
🪛 Stylelint (17.5.0)
[error] 330-330: Expected "currentColor" to be "currentcolor" (value-keyword-case)
(value-keyword-case)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/ui/src/styles/index.css` at line 330, The CSS declaration "border:
1px solid currentColor" uses camelCase and violates the Stylelint
value-keyword-case rule; update the declaration by replacing the token
currentColor with lowercase currentcolor (i.e., change the value in the "border:
1px solid currentColor" rule), then re-run stylelint to confirm the violation is
resolved.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
/healthevery 30 s, reloads on.retortconfigchange, emitscogmesh:health:updatedcogmesh.endpoint/secretwith${VAR}env substitutionCognitiveMeshHealthunion, 7 CM escalation fields onAgentTask'cogmesh'tab +CogmeshDotstatus indicatorTest plan
connectedunreachableafter 5 s timeout.retortconfigupdated → poller reloads within 30 sCloses #2
🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
.retortconfigfile.